Treat assignments in unreachable code as local bindings - #11666
Treat assignments in unreachable code as local bindings#11666Henry Su (hsusul) wants to merge 2 commits into
Conversation
CPython treats any assignment in a function as making the name local, even after a return. Skipping those statements left later-assigned names looking global and hid UnboundLocalError. Fixes microsoft#11449
|
🔒 Automated review in progress — Rich Chiodo (@rchiodo) is auto-reviewing this PR. |
| GlobalNode, | ||
| IfNode, | ||
| ImportAsNode, | ||
| ImportFromAsNode, |
There was a problem hiding this comment.
Issue · Please address or respond
ImportFromAsNode is unused by this change, so this will fail unused-import checks. Remove the import.
| return false; | ||
| } | ||
|
|
||
| override visitFunction(node: FunctionNode): boolean { |
There was a problem hiding this comment.
Warning · Non-blocking recommendation
This walker skips function/class headers but descends into lambda bodies. That misses assignment expressions in decorators, defaults, and class bases that bind in the enclosing scope, while incorrectly treating assignment expressions in a lambda body as enclosing-scope bindings. Please handle those scope boundaries explicitly and add regressions for both cases.
| this._bindName(node.d.target); | ||
| } | ||
| return true; | ||
| } |
There was a problem hiding this comment.
Issue · Please address or respond
UnreachableNameBinder does not process global or nonlocal declarations. Consequently, an unreachable global x; x = ... or nonlocal x; x = ... binds the assignment directly in the current scope, incorrectly making it local. Preserve the declarations' binding semantics before binding targets and cover unreachable declaration cases.
|
Verification: The relevant tests could not be fully run in the isolated environment; this review is not fully verified. |
| // local for the entire function). Nested functions and classes are skipped | ||
| // because DummyScopeGenerator already created their scopes. | ||
| class UnreachableNameBinder extends ParseTreeWalker { | ||
| constructor( |
There was a problem hiding this comment.
Warning · Non-blocking recommendation
Unreachable global and nonlocal directives are not processed, so a later assignment can be bound to the current local scope rather than the declared scope. Handle these directives before binding targets and add focused unreachable-directive coverage.
|
|
||
| override visitPatternCapture(node: PatternCaptureNode): boolean { | ||
| this._bindName(node.d.target); | ||
| return false; |
There was a problem hiding this comment.
Warning · Non-blocking recommendation
UnreachableNameBinder has no lambda scope boundary. Walking lambda: (x := 1) will bind x in the enclosing scope even though the assignment expression belongs to the lambda's scope. Skip lambda bodies and add a regression case.
| override visitTypeAlias(node: TypeAliasNode): boolean { | ||
| this._bindName(node.d.name); | ||
| return false; | ||
| } |
There was a problem hiding this comment.
Warning · Non-blocking recommendation
Returning false for nested functions and classes skips decorators, default values, bases, and keywords, which are evaluated in the enclosing scope. Assignment expressions in those header expressions therefore miss their enclosing binding; traverse the headers while excluding only nested bodies.
| this._bindName(node.d.target); | ||
| return false; | ||
| } | ||
| } |
There was a problem hiding this comment.
Warning · Non-blocking recommendation
This new visitor duplicates the primary binder's syntax and scope classification, and the missing directive and scope-boundary cases already demonstrate drift. Reuse or centralize the scope-aware binding classification so future syntax additions do not update only one path.
|
Verification: The relevant tests could not be fully run in the isolated environment; this review is not fully verified. |
Rich Chiodo (rchiodo)
left a comment
There was a problem hiding this comment.
Approved via Review Center.
Rich Chiodo (rchiodo)
left a comment
There was a problem hiding this comment.
Please address the comments in the binder.ts
|
Verification: The relevant tests could not be fully run in the isolated environment; this review is not fully verified. |
|
Addressed the binder feedback in 3268d67:
Validation passed for the focused Unbound tests (7/7), neighboring Unreachable1 test, The full internal Jest run passed 69/71 suites and 2,716/2,718 tests. The two failures were in unrelated areas: DataClassReplace1 in typeEvaluator4.test.ts and Builtins1 in typeEvaluator1.test.ts. Neither failure touches the changed files or binder behavior in this revision. I kept the broader binding-classification centralization suggestion out of this targeted correctness fix to avoid expanding the PR beyond the demonstrated scope-boundary and directive issues. |
| this._bindName(node.d.target); | ||
| return false; | ||
| } | ||
| } |
There was a problem hiding this comment.
Warning · Non-blocking recommendation
UnreachableNameBinder independently classifies binding syntax and scope boundaries. Centralize or reuse the primary binder's scope-aware classification so future syntax changes cannot silently diverge.
[verified]
|
Verification: The relevant tests could not be fully run in the isolated environment; this review is not fully verified. |
Summary
return.UnboundLocalErrorwas missed. Nestednonlocallookups into such names also failed.Test plan
npx jest typeEvaluator2.test.ts -t "Unbound" --forceExit(frompackages/pyright-internal)npx jest typeEvaluator1.test.ts -t "Unreachable1" --forceExitreturnreports an unbound variable in the language server